/**
 * Licensed to Jasig under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Jasig licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a
 * copy of the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2010, Red Hat Inc. or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat Inc.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 */

package org.jasig.portal.utils;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.hibernate.id.IdentifierGeneratorHelper;
import org.hibernate.id.IntegralDataTypeHolder;
import org.hibernate.id.enhanced.AccessCallback;
import org.hibernate.id.enhanced.Optimizer;
import org.hibernate.id.enhanced.OptimizerFactory;
import org.hibernate.id.enhanced.TableGenerator;
import org.hibernate.type.IntegerType;
import org.hibernate.type.Type;
import org.jasig.portal.jpa.BasePortalJpaDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionOperations;

/**
 * Mostly cloned from {@link TableGenerator}
 * 
 * @author Eric Dalquist
 * @version $Revision$
 */
@Repository("counterStore")
public class HibernateStyleCounterStore implements ICounterStore {
    private static final String SELECT_QUERY = "SELECT SEQUENCE_VALUE FROM UP_SEQUENCE WHERE SEQUENCE_NAME=?";
    private static final String UPDATE_QUERY = "UPDATE UP_SEQUENCE SET SEQUENCE_VALUE=? WHERE SEQUENCE_NAME=? AND SEQUENCE_VALUE=?";
    private static final String INSERT_QUERY = "INSERT INTO UP_SEQUENCE (SEQUENCE_NAME, SEQUENCE_VALUE) VALUES (?, ?)";

    private final Type identifierType = IntegerType.INSTANCE;

    private final ConcurrentMap<String, Callable<Optimizer>> counterOptimizers = new ConcurrentHashMap<String, Callable<Optimizer>>();
    private TransactionOperations transactionOperations;
    private JdbcOperations jdbcOperations;
    private int incrementSize = 50;
    private int initialValue = 10;
    
    @Autowired
    public void setTransactionOperations(@Qualifier(BasePortalJpaDao.PERSISTENCE_UNIT_NAME) TransactionOperations transactionOperations) {
        this.transactionOperations = transactionOperations;
    }

    @Autowired
    public void setJdbcOperations(@Qualifier(BasePortalJpaDao.PERSISTENCE_UNIT_NAME) JdbcOperations jdbcOperations) {
        this.jdbcOperations = jdbcOperations;
    }

    @Value("${org.jasig.portal.utils.HibernateStyleCounterStore.incrementSize:50}")
    public void setIncrementSize(int incrementSize) {
        this.incrementSize = incrementSize;
    }

    @Value("${org.jasig.portal.utils.HibernateStyleCounterStore.initialValue:10}")
    public void setInitialValue(int initialValue) {
        this.initialValue = initialValue;
    }

    private Optimizer getCounterOptimizer(String counterName) {
        Callable<Optimizer> optimizer = counterOptimizers.get(counterName);
        if (optimizer == null) {
            optimizer = new Callable<Optimizer>() {
                private volatile Optimizer optimizer;
                
                @Override
                public Optimizer call() throws Exception {
                    Optimizer o = optimizer;
                    if (o != null) {
                        return o;
                    }
                    
                    synchronized (this) {
                        o = optimizer;
                        if (o != null) {
                            return o;
                        }
                    
                        o = OptimizerFactory.buildOptimizer(
                            OptimizerFactory.StandardOptimizerDescriptor.POOLED.getExternalName(), 
                            identifierType.getReturnedClass(), 
                            incrementSize, 
                            initialValue);
                        this.optimizer = o;
                    }
                    
                    return o;
                }
            };

            optimizer = ConcurrentMapUtils.putIfAbsent(counterOptimizers, counterName, optimizer);
        }

        try {
            return optimizer.call();
        }
        catch (Exception e) {
            if (e instanceof RuntimeException) {
                throw (RuntimeException) e;
            }
            throw new RuntimeException(e);
        }
    }

    @Override
    public int getNextId(String counterName) {
        final int id;
        final Optimizer counterOptimizer = this.getCounterOptimizer(counterName);
        synchronized (counterOptimizer) {
            id = getNextIdInternal(counterOptimizer, counterName);
        }
        
        return id;
    }

    private int getNextIdInternal(final Optimizer optimizer, final String counterName) {
        return (Integer) optimizer.generate(new AccessCallback() {
            @Override
            public IntegralDataTypeHolder getNextValue() {
                return transactionOperations.execute(new TransactionCallback<IntegralDataTypeHolder>() {
                    @Override
                    public IntegralDataTypeHolder doInTransaction(TransactionStatus status) {
                        final IntegralDataTypeHolder value = IdentifierGeneratorHelper.getIntegralDataTypeHolder(identifierType.getReturnedClass());
                        int rows;
                        do {
                            //Try and load the current value, returns true if the exepected row exists, null otherwise
                            final boolean selected = jdbcOperations.query(SELECT_QUERY, new ResultSetExtractor<Boolean>() {
                                        @Override
                                        public Boolean extractData(ResultSet rs) throws SQLException, DataAccessException {
                                            if (rs.next()) {
                                                value.initialize(rs, 1);
                                                return true;
                                            }
                                            return false;
                                        }
                                }, counterName);
                            
                            //No row exists for the counter, insert it
                            if (!selected) {
                                value.initialize(initialValue);
                                
                                jdbcOperations.update(INSERT_QUERY, new PreparedStatementSetter() {
                                    @Override
                                    public void setValues(PreparedStatement ps) throws SQLException {
                                        ps.setString(1, counterName);
                                        value.bind(ps, 2);
                                    }
                                });
                            }
                            
                            //Increment the counter row value
                            final IntegralDataTypeHolder updateValue = value.copy();
                            if (optimizer.applyIncrementSizeToSourceValues()) {
                                updateValue.add(incrementSize);
                            }
                            else {
                                updateValue.increment();
                            }
                            
                            //Update the counter row, if rows returns 0 the update failed due to a race condition, it will be retried
                            rows = jdbcOperations.update(UPDATE_QUERY, new PreparedStatementSetter() {
                                @Override
                                public void setValues(PreparedStatement ps) throws SQLException {
                                    updateValue.bind(ps, 1);
                                    ps.setString(2, counterName);
                                    value.bind(ps, 3);
                                }
                            });
                        } while (rows == 0);

                        return value;
                    }
                });
            }
        });
    }
}
